﻿#region [ License information          ]
/* ************************************************************
 *
 * Copyright (c) Bjørn Bouet Smith, 2010, 2011
 *
 * This source code is subject to terms and conditions of 
 * Microsoft Public License (Ms-PL).
 * 
 * A copy of the license can be found in the license.txt
 * file at the root of this distribution. If you can not 
 * locate the License, please send an email to bjornsmith@gmail.com
 * 
 * By using this source code in any fashion, you are 
 * agreeing to be bound by the terms of the Microsoft 
 * Permissive License.
 *
 * You must not remove this notice, or any other, from this
 * software.
 *
 * ************************************************************/
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.ServiceModel.Channels;
using Smith.AMC.Operations;
using Wintellect.Threading.AsyncProgModel;

namespace Smith.AMC.Network
{
    /// <summary>
    /// Implementation of the <see cref="Socket"/> class that can write a Memcached command to the socket and return the response from the server.
    /// </summary>
    public class ClientSocket : Socket, IClientSocket
    {
        /// <summary>
        /// The IPEndpoint this socket should connect to
        /// </summary>
        protected readonly IPEndPoint IpEndPoint;

        /// <summary>
        /// The size of the receive buffer
        /// </summary>
        private const int BufferSize = 10000;

        /// <summary>
        /// The total size of the buffers
        /// </summary>
        private const int TotalBufferSize = BufferSize * 100;

        /// <summary>
        /// Buffer manager that handles managing the buffers used for temporarily storing the data received from the server.
        /// </summary>
        private static readonly BufferManager BufferManager = BufferManager.CreateBufferManager(TotalBufferSize, BufferSize);

        /// <summary>
        /// Initializes a new instance of the <see cref="ClientSocket"/> class.
        /// </summary>
        /// <param name="ipEndPoint">The ip end point.</param>
        /// <param name="socketType">Type of the socket.</param>
        /// <param name="protocol">The protocol.</param>
        public ClientSocket(
            IPEndPoint ipEndPoint,
            SocketType socketType,
            ProtocolType protocol) :
            base(ipEndPoint.AddressFamily, socketType, protocol)
        {
            IpEndPoint = ipEndPoint;
            if (socketType != SocketType.Dgram && socketType != SocketType.Stream)
            {
                throw new ArgumentException("Only SocketType.Dgram and SocketType.Stream is supported", "socketType");
            }

            if (protocol != ProtocolType.Udp &&
                protocol != ProtocolType.Tcp)
            {
                throw new ArgumentException("Only .ProtocolType.Udp and ProtocolType.Tcp is supported", "protocol");
            }

            ConfigureSocket();
        }

        /// <summary>
        /// Gets or sets the owner ID of this socket.
        /// This ID is the ID of the memcached server
        /// </summary>
        public int OwnerID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="IClientSocket"/> is dead.
        /// </summary>
        /// <value></value>
        public bool Dead
        {
            get;
            set;
        }

        /// <summary>
        /// Begins an asynchronous write operation against the memcached server.
        /// </summary>
        /// <typeparam name="T">The type of data to add.</typeparam>
        /// <param name="operation">The operation to write to the socket.</param>
        /// <param name="asyncCallBack">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginAdd operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginWrite<T>(Operation operation, AsyncCallback asyncCallBack, object state)
        {
            AsyncEnumerator<OperationResponse<T>> ae = new AsyncEnumerator<OperationResponse<T>>();
            return ae.BeginExecute(DoWrite(ae, operation), asyncCallBack, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous write operation
        /// </summary>
        /// <typeparam name="T">Type of item added</typeparam>
        /// <param name="result">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// true if the add operation was a success; false otherwise
        /// </returns>
        public OperationResponse<T> EndWrite<T>(IAsyncResult result)
        {
            return AsyncEnumerator<OperationResponse<T>>.FromAsyncResult(result).EndExecute(result);
        }

        /// <summary>
        /// Method that does the actual write.
        /// </summary>
        /// <typeparam name="T">Type of data the operation contains</typeparam>
        /// <param name="ae">The async enumerator.</param>
        /// <param name="operation">The operation.</param>
        /// <returns>An Ienumerator that the async enumenrator iterates over</returns>
        protected virtual IEnumerator<int> DoWrite<T>(AsyncEnumerator<OperationResponse<T>> ae, Operation operation)
        {
            if (!Connected)
            {
                BeginConnect(IpEndPoint, ae.End(), null);
                yield return 1;
                EndConnect(ae.DequeueAsyncResult());
            }

            List<ArraySegment<byte>> arraySegments = operation.ToPackageFormat();

            SocketError err;

            // Send buffer
            BeginSend(arraySegments, SocketFlags.None, out err, ae.End(), null);
            yield return 1;

            EndSend(ae.DequeueAsyncResult());
            
            // Grab buffer from buffer manager
            byte[] readBuffer = BufferManager.TakeBuffer(BufferSize);

            try
            {
                BeginReceive(readBuffer, 0, BufferSize, SocketFlags.None, out err, ae.End(), null);
                yield return 1;
                int bytesRead = EndReceive(ae.DequeueAsyncResult());
                MemoryStream ms = new MemoryStream(bytesRead * 2);
                while (bytesRead > 0)
                {
                    ms.Write(readBuffer, 0, bytesRead);

                    if (bytesRead != BufferSize)
                    {
                        break;
                    }

                    if (readBuffer[BufferSize - 1] == (byte)'\n' && readBuffer[BufferSize - 2] == (byte)'\r')
                    {
                        // Even though we read a full buffer, there are no more data to read
                        // since last part of buffer is \r\n
                        break;
                    }

                    // More data to read
                    BeginReceive(readBuffer, 0, BufferSize, SocketFlags.None, ae.End(), null);
                    yield return 1;

                    bytesRead = EndReceive(ae.DequeueAsyncResult());
                }

                // all done - return result in the asyncenumerator
                ae.Result = new OperationResponse<T>
                                {
                                    ServerResponse = ms.ToArray(),
                                    CacheKey = operation.Key,
                                    CASValue = 0,
                                    OperationName = operation.Name
                                };
            }
            finally
            {
                // release buffer back into buffer manager
                BufferManager.ReturnBuffer(readBuffer);
            }
        }

        /// <summary>
        /// Configures the socket
        /// </summary>
        private void ConfigureSocket()
        {
            if (ProtocolType == ProtocolType.Tcp)
            {
                NoDelay = true;
            }
            ReceiveBufferSize = 125000;
            //UseOnlyOverlappedIO = true;
        }
    }
}
